﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;


namespace Summerset.Isbndb
{

    /// <summary>
    /// Represents a collection of pages returned as part of a response from the API.
    /// </summary>
    /// <typeparam name="PageType">The type of page that will contain the elements.</typeparam>
    /// <typeparam name="ElementType">The individual elements that are returned in each page.  </typeparam>
    public sealed class PagesCollection<PageType, ElementType> :
        IEnumerable, IEnumerable<PageType>, INotifyPropertyChanged
        where PageType : ResponsePageBase<ElementType>
        where ElementType : ResponseElementBase
    {
        #region Constructor + Methods + Event Handlers


        /// <summary>
        /// Initializes a new instance of the PagesCollection class.  It's important to note that regardless 
        /// of how the PageLoadingMethod is configured for the parent response, the first 'Page' of this collection will always
        /// be loaded syncronously by this constructor.   
        /// </summary>
        /// <param name="Parent">The response object that this collection should populate itself with data for. </param>
        public PagesCollection(Response<PageType, ElementType> Parent)
        {

            // Set the local reference to our parent object, and listen for our own PageLoaded event
            this.ParentResponse = Parent;
            this.PageLoading += new EventHandler(BooksResponsePageCollection_PageLoading);
            this.PageLoaded += new EventHandler<PageLoadedEventArgs>(BooksResponsePageCollection_PageLoaded);
            this.PageLoadingComplete += new EventHandler(BooksResponsePageCollection_PageLoadingComplete);

            // The first page always must be loaded on the same thread as the parent response.
            // Certain properties of BooksResponse rely on having at least 1 page response.  
            this.LoadPage(1);

        }


        /// <summary>
        /// An overload to allow a ParameterizedThreadStart delegate to be created to call this method. 
        /// </summary>
        /// <param name="pageNumber">The page number.</param>
        public void LoadPage(object pageNumber)
        {

            // Make sure the argument is a good one
            if (pageNumber.GetType() != typeof(int))
                throw new ArgumentException("The argument PageNumber must be castable to type 'int'");

            // Call the other overload of this method.
            LoadPage(Convert.ToInt32(pageNumber));
        }
        /// <summary>
        /// Creates a new page object and inserts it into the proper index in this collection (e.g. index = PageNumber - 1)
        /// </summary>
        /// <param name="pageNumber">The page number.  This value must be greater than 1, but must be less than the total number of pages requested.</param>
        public void LoadPage(int pageNumber)
        {
            try
            {

                /* 
                 * Perform validation on the provided PageNumber argument.  We're going to make sure that we reference the _pages field, and 
                 * we don't want to use this class' indexer yet.  The indexer will initialize
                 * 1st - cannot be less than 1, or greater than the maximum number of expected pages
                 * 2nd - cannot reload a page that's already been loaded 
                 */

                if (pageNumber <= 0)
                    throw new ArgumentException("PageNumber argument must be 1 or greater.");
                if (_pages != null && pageNumber > _pages.Count())
                    throw new ArgumentException(string.Format("PageNumber ({0}) argument cannot be greater than the maximum number of pages ({1})", pageNumber, _pages.Count()));
                if (_pages != null && _pages[pageNumber - 1] != null)
                    throw new InvalidOperationException(string.Format("Page {0} has already been loaded.", pageNumber));


                // Notify any listeners that we're starting to load this page
                if (this.PageLoading != null)
                    this.PageLoading(this, new EventArgs());


                // Now, we need to get our page. 
                // We need to use reflection to get the constructor for our page type
                var ctorInfo = typeof(PageType).GetConstructor(new Type[] { typeof(int), typeof(Uri) });
                var page = (PageType)ctorInfo.Invoke(new object[] 
                { 
                    pageNumber + 1, 
                    this.ParentResponse.Request.GetRequestFullUri(pageNumber + 1, this.ParentResponse.Owner)
                });


                // If the array hasn't been initialized, we need to figure out how many pages we're going to expect at most.
                if (_pages == null)
                {

                    // There are always 0-10 results per page.

                    // Get the total number of 'whole pages' -- always make sure there is at least one page, even if there are 0 results
                    int maximumPages = 0;
                    if (page.MaximumResults == 0)
                        maximumPages = 1;
                    else
                        maximumPages = (page.MaximumResults / 10);

                    // See if there is a final, 'partial page' 
                    if (page.MaximumResults % 10 != 0)
                        maximumPages++;


                    _pages = new PageType[maximumPages];

                }


                // After we've made sure the pages array has been initialized, we can add 
                _pages[pageNumber - 1] = page;


                // Request was completed without errors
                if (PageLoaded != null)
                    PageLoaded(_pages[pageNumber - 1], new PageLoadedEventArgs());

            }
            catch (Exception ex)
            {

                Console.WriteLine("Error Loading Page {0} - {1}", pageNumber, ex.Message);

                // Raise the page loaded event but pass the exception along
                if (PageLoaded != null)
                    PageLoaded(_pages[pageNumber - 1], new PageLoadedEventArgs(ex));
            }

        }

        /// <summary>
        /// Nexts the index of the null page.
        /// </summary>
        /// <returns></returns>
        public Nullable<int> NextNullPageIndex()
        {

            Debug.WriteLine("Looking for the next null page index");
            Debug.WriteLine("Looks like we've got {0} total pages and {1} expected results.", _pages.Count(), this[0].MaximumResults);

            for (int i = 0; i < _pages.Count(); i++)
            {

                Debug.WriteLine("Checking Page {0}", i + 1);


                // Found a null space in the pages array
                if (_pages[i] == null)
                {
                    Debug.WriteLine("Found it! Page {0} (index {1} -- this is the value that'll be returned) was null!", i + 1, i);
                    return i;
                }
            }

            // Couldn't find a null page -- pages collection has been fully initialized
            return null;
        }


        // Increments the 'Requests Generated' counter forward.
        void BooksResponsePageCollection_PageLoading(object sender, EventArgs e)
        {
            this.RequestsGenerated++;
        }

        // When set to async or sync loading of pages, this method handles loading the next avaialble page.
        void BooksResponsePageCollection_PageLoaded(object sender, PageLoadedEventArgs e)
        {


            // Now, we need to check if we can stop loading additional pages
            if (e.Exception != null)
            {

                // There was a problem loading this page -- so we're going to stop trying to load additional pages and we're going to report that we're finished.
                this.PageLoadingError = e.Exception;

                if (this.PageLoadingComplete != null)
                    PageLoadingComplete(this, new EventArgs());

                return;
            }
            else if (!this.NextNullPageIndex().HasValue)
            {

                // Every element has been populated, so we're done!
                if (this.PageLoadingComplete != null)
                    PageLoadingComplete(this, new EventArgs());

                return;
            }



            switch (ParentResponse.Owner.PageLoadingBehavior)
            {

                // In this case, we want to find the next 'page' that has a null element, and we want to load that.
                case PageLoadingBehavior.AsyncLoadOnInitialization:

                    // We found the next null page -- we're going to create a thread and load the next page on this new thread.
                    var pageLoadThread = new Thread(this.LoadPage);
                    pageLoadThread.Name = "Books Response Page Loading";
                    pageLoadThread.Start(((object)(this.NextNullPageIndex().Value + 1)));
                    break;


                // In this case, we're loading all pages one-at-a-time on this same thread.
                case PageLoadingBehavior.SyncLoadOnInitialization:

                    // Load the next new page on this same thread
                    this.LoadPage(this.NextNullPageIndex().Value + 1);

                    break;

                default:
                    // All other cases don't require anything special
                    break;
            }


        }

        // Just update the IsPageLoadComplete property, which will raise it's own PropertyChanged event.
        void BooksResponsePageCollection_PageLoadingComplete(object sender, EventArgs e)
        {

            this.IsPageLoadComplete = true;

        }

        #endregion
        #region Properties + Fields

        /// <summary>
        /// Gets the parent response.
        /// </summary>
        public Response<PageType, ElementType> ParentResponse { get; private set; }

        /// <summary>
        /// Gets the page at the specified index.
        /// </summary>
        public PageType this[int index]
        {
            get
            {

                /*
                 * There are 2 things that can happen here:
                 * 1 - the requested element is null.  In that case, we need to examine the parent object, and see what it's PageLoadingBehavior is set to, and how we need to response.
                 * 2 - the requested element is !null, so we can just return it to the client.
                 * 
                 */

                if (_pages[index] == null)
                {

                    // Now we need to look at the parent, and see how we're supposed to handle null behavior. 
                    switch (this.ParentResponse.PageLoadingBehavior)
                    {
                        case PageLoadingBehavior.SyncLoadOnRequest:
                            // We need to load the page now. -- because we're unsure of the underlying generic type, we'll use reflection to get the constructor that we're sure about
                            LoadPage(index + 1);
                            return _pages[index];

                        case PageLoadingBehavior.ReturnNull:
                            // We're just going to return a null page.  The client code will need to manually load this page if it wants it.
                            return null;

                        case PageLoadingBehavior.AsyncLoadOnInitialization:
                            // This can happen if the user requests a page too quickly.  We'll return null.
                            return null;

                        case PageLoadingBehavior.SyncLoadOnInitialization:
                            // This is bad news bears.  
                            throw new InvalidOperationException("This collection is configured to load on initialization, however this index is null.  Please reload this request.");

                        default:
                            throw new InvalidOperationException("Could not determine the expected PageLoadingBehavior for this BooksResponse.");
                    }
                }
                else
                {

                    // The requested page exists, so we'll just return it.
                    return _pages[index];
                }


            }
            private set { _pages[index] = value; }
        }
        PageType[] _pages;

        /// <summary>
        /// Gets the total number of BooksResponsePage objects that occur in this collection.  
        /// This value also includes pages that are currently null.
        /// </summary>
        /// <remarks>Depending on how the Parent's PageLoadingBehavior is configured, it's important to know that not all the pages (beyond the page at index 0) will necessarially have a non-null value.  This value indicates the total number of pages that will occur when this collection is fully populated.</remarks>
        public int Count
        {
            get
            {
                if (_pages == null)
                    return -1;
                else
                    return _pages.Count();
            }
        }

        /// <summary>
        /// Gets a count of pages in this collection that are currently null.
        /// </summary>
        public int NullPagesCount
        {
            get
            {

                int i = 0;

                // Loop through the _pages array, but we're going to avoid accessing this through the indexer.
                // If the parent response is configured to load pages on request, this would cause all pages to load .. so, this property would always return 0.
                foreach (var pg in _pages) if (pg == null) ++i;

                return i;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the pages have completed their loading process.  
        /// This will return <c>true</c> when either all the required page objects
        /// have been loaded, or an exception has been countered during a page load and page loading has been terminated.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is page load complete; otherwise, <c>false</c>.
        /// </value>
        public bool IsPageLoadComplete
        {
            get
            {
                return _isPageLoadComplete;
            }
            private set
            {
                if (value != _isPageLoadComplete)
                {
                    _isPageLoadComplete = value;

                    if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsPageLoadComplete"));
                }
            }
        }
        bool _isPageLoadComplete = false;

        /// <summary>
        /// Gets the most recent error encountered while loading page
        /// instances into this collection.  If no errors have been countered, a null value will be
        /// returned.
        /// </summary>
        public Exception PageLoadingError
        {
            get { return _pageLoadingError; }
            private set
            {
                if (value != _pageLoadingError)
                {

                    _pageLoadingError = value;

                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs("PageLoadingError"));
                }
            }
        }
        Exception _pageLoadingError = null;

        /// <summary>
        /// Gets the initial request server time.
        /// </summary>
        public DateTimeOffset InitialRequestServerTime
        {
            get
            {

                // Get the server times from any pages that aren't null.
                // We need to make sure that we're making these types of requests (that iterate over ever element in _pages)
                // directly against _pages, and not through this class's indexer.  If the parent is configured to SyncLoadOnRequest
                // any access to every page in the index will cause every element to load.
                var serverResponseTimes = from pg in _pages
                                          where pg != null
                                          select pg.ServerResponseTime;

                return serverResponseTimes.Min();
            }
        }

        /// <summary>
        /// Gets the latest request server time.
        /// </summary>
        public DateTimeOffset LatestRequestServerTime
        {
            get
            {

                // Get the server times from any pages that aren't null.
                // We need to make sure that we're making these types of requests (that iterate over ever element in _pages)
                // directly against _pages, and not through this class's indexer.  If the parent is configured to SyncLoadOnRequest
                // any access to every page in the index will cause every element to load.
                var serverResponseTimes = from pg in _pages
                                          where pg != null
                                          select pg.ServerResponseTime;

                return serverResponseTimes.Max();
            }
        }

        /// <summary>
        /// Gets the latest keystats data from the most recent request.
        /// </summary>
        public Keystats LatestKeystatsData
        {
            get
            {

                // DO NOT make this request through the indexer property.
                // This needs to be made directly against the _pages field.
                // If requested through the indexer property, each field will be initialized as the request is made.
                var getKeystats = from pg in _pages
                                  where pg != null
                                  orderby pg.ServerResponseTime descending
                                  select pg.Keystats;


                return getKeystats.FirstOrDefault();

            }
        }

        /// <summary>
        /// Gets the number of requests generated against this API by the parent request.
        /// NOTE: this is NOT the number of total requests generated against the API for the day -- only
        /// the requests generated by the parent request.
        /// </summary>
        public int RequestsGenerated
        {
            get
            {
                return _requestsGenerated;
            }
            private set
            {
                if (RequestsGenerated != value)
                {
                    _requestsGenerated = value;
                    if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("RequestsGenerated"));
                }
            }
        }
        int _requestsGenerated = 0;

        #endregion
        #region Events

        /// <summary>
        /// Occurs just before a page begins to request it's data from the ISBNdb API.
        /// </summary>
        public event EventHandler PageLoading;

        /// <summary>
        /// Occurs when after a page has been initialized or this event will also be raised when a page
        /// fails to initialize itself (in which case, the EventArgs will include a reference to the Exception that was thrown).        
        /// </summary>
        public event EventHandler<PageLoadedEventArgs> PageLoaded;

        /// <summary>
        /// Occurs when when all pages have been loaded.  
        /// This event is only raised if the parent <see cref="Isbndb"/>'s PageLoadingBehavior
        /// is set to AsyncLoadOnInitialization, or SyncLoadOnInitialization.        
        /// </summary>
        public event EventHandler PageLoadingComplete;

        #endregion        
        #region IEnumerable Members

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator GetEnumerator()
        {
            return new PagesEnumerator<PageType, ElementType>(this);
        }

        #endregion
        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion            
        #region IEnumerable<T> Members

        IEnumerator<PageType> IEnumerable<PageType>.GetEnumerator()
        {
            return new PagesEnumerator<PageType, ElementType>(this);
        }

        #endregion
    }
}
